//! NETCONF over SSH Call Home based on RFC8071.
//! Implement of [ietf-netconf-server@2019-07-02.yang](/datastore/yang/ietf-netconf-server@2019-07-02.yang) feature ssh call home.
//!

use log::{error, info};
use std::{sync::Arc, time::Duration};
use tokio::{net::TcpStream, sync::broadcast, time::sleep};

use crate::{ctx::load_context, ssh::NetconfHandler};

#[allow(unused)]
#[derive(Debug, Clone)]
pub struct CallHomeServerCfg {
    clients: Vec<NcClientCfg>,
}

impl CallHomeServerCfg {
    pub fn new(remote_client_cfg: &[(&str, u16)]) -> Self {
        Self {
            clients: remote_client_cfg
                .iter()
                .map(|cfg| NcClientCfg {
                    name: cfg.0.to_string(),
                    endpoints: vec![NcEndpoint {
                        name: cfg.0.to_string(),
                        transport: SshTransport {
                            tcp_client_parameters: TcpClientParams {
                                remote_address: cfg.0.to_string(),
                                remote_port: cfg.1,
                                local_address: "0.0.0.0".to_string(),
                                local_port: 0,
                                keepalives: TcpKeepalives {
                                    idle_time: 3600,
                                    max_probes: 120,
                                    probe_interval: 120,
                                },
                            },
                        },
                    }],
                    connection_type: ConnectionType::Persistent,
                    reconnect_strategy: ReconnectionStrategy {
                        start_with: StartWith::FirstListed,
                        max_attempts: 3,
                    },
                })
                .collect(),
        }
    }
}

#[allow(unused)]
#[derive(Debug, Clone)]
pub(crate) struct NcClientCfg {
    name: String,
    endpoints: Vec<NcEndpoint>,
    connection_type: ConnectionType,
    reconnect_strategy: ReconnectionStrategy,
}

#[allow(unused)]
#[derive(Debug, Clone)]
pub(crate) struct ReconnectionStrategy {
    start_with: StartWith,
    max_attempts: u8,
}

#[allow(unused)]
#[derive(Debug, Clone, Default)]
pub(crate) enum StartWith {
    #[default]
    FirstListed,
    LastConnected,
    TandomSelection,
}

#[allow(unused)]
#[derive(Debug, Clone)]
pub(crate) enum ConnectionType {
    Persistent,
    Periodic,
}

#[allow(unused)]
#[derive(Debug, Clone)]
pub(crate) struct NcEndpoint {
    name: String,
    transport: SshTransport,
}

#[allow(unused)]
#[derive(Debug, Clone)]
pub(crate) struct SshTransport {
    tcp_client_parameters: TcpClientParams,
}

#[allow(unused)]
#[derive(Debug, Clone)]
pub(crate) struct TcpClientParams {
    remote_address: String,
    remote_port: u16,
    local_address: String,
    local_port: u16,
    keepalives: TcpKeepalives,
}

#[allow(unused)]
#[derive(Debug, Clone)]
pub(crate) struct TcpKeepalives {
    idle_time: u16,
    max_probes: u16,
    probe_interval: u16,
}

pub async fn run(config: &CallHomeServerCfg) -> Result<(), anyhow::Error> {
    loop {
        let client = &config.clients[0].endpoints[0];
        let remote_addr = &client.transport.tcp_client_parameters.remote_address;
        let remote_port = client.transport.tcp_client_parameters.remote_port;
        let tcp_stream = TcpStream::connect(&format!("{}:{}", remote_addr, remote_port)).await?;
        let mut config = russh::server::Config::default();
        config
            .keys
            .push(russh_keys::key::KeyPair::generate_ed25519().unwrap());
        let config = Arc::new(config);
        let (tx, rx) = broadcast::channel(100);
        let sh = NetconfHandler {
            channel_id: None,
            data_cache: vec![],
            threads: 1,
            ctx: load_context()?,
            rx,
            tx,
        };
        let session = russh::server::run_stream(config, tcp_stream, sh).await;
        match session {
            Ok(session) => match session.await {
                Ok(()) => {
                    info!("end");
                }
                Err(e) => {
                    error!("session error: {}", e.to_string());
                    sleep(Duration::from_secs(5)).await;
                    continue;
                }
            },
            Err(e) => {
                error!("session error: {}", e.to_string());
                sleep(Duration::from_millis(50)).await;
                continue;
            }
        }
    }
}

#[cfg(test)]
mod tests {

    use crate::ssh_call_home::{run, CallHomeServerCfg};

    #[tokio::test]
    async fn test_run() -> Result<(), anyhow::Error> {
        run(&CallHomeServerCfg::new(&[("192.168.1.11", 8400)])).await?;
        Ok(())
    }
}
